算法动画 - 理解函数曲线
这篇梳理一些有关算法动画的生成思路。
用算法生成动画,大致可分成两类。一类是基于时间( time-based ),一类是基于帧( frame-based )。其中有何区别,我们先通过两段 Processing 代码去理解。
代码 01( 基于帧 )
float x;
void setup(){
size(600,200);
x = 100;
}
void draw(){
background(239,234,228);
if(x < 500){
x += 5;
}
fill(50,120,133);
noStroke();
ellipse(x,height/2,50,50);
}
代码浅析:
代码中创建了一个变量 x 表示圆的横坐标。数值初始化为 100。而draw 函数中 x 每次累加 5,直到 500 停止累加。因此 x 的数值变化范围则是从 100 到 500,实现了小球从左到右的运动
代码 02 ( 基于时间 )
float time;
void setup(){
size(600,200);
time = 3;
}
void draw(){
background(239,234,228);
float x = min(500,map(millis()/1000.0,0,time,100,500));
fill(50,120,133);
noStroke();
ellipse(x,height/2,50,50);
}
代码浅析:
代码中创建了一个变量 time ,表示圆从左侧运动到右侧的时间
millis() 表示毫秒,因而 millis()/1000.0 表示秒。通过 map 函数,将时间从 0 到 time 的变化,映射为从 100 到 500 的变化。随着时间的递增,实现了小球从左到右的运动
min 函数用于限定 x 的大小,让数值不超过 500
简单比较
通过对比两段代码可以发现,虽然最终结果是近似的(小球从 100 匀速运动到 500),但决定运动的条件是不同的。前者限定了每次小球每次递增的距离,后者限定了整个运动的时间。
条件的不同,决定了在某些场景下,某种方法会比另一种方法使用起来更便利。例如要绘制一个运行速度恒定的小车,使用基于帧的算法写起来会更简便。若希望小车从 A 点运行到 B 点的时间是固定值,又或者实现时间间隔固定的淡入淡出效果(将数值变化映射到颜色变化),基于时间的算法则更合适。
除此之外,它们两者间还有一个更重要的区别。使得自己在制作动画时,更倾向使用基于时间的思路。
前面的例子中,由于绘制的都是一些非常简单的图形,所以程序运行必然非常流畅平稳的,维持在 60 fps。但如果程序有复杂的场景切换。某些场景绘制的元素多,占用更多计算资源。就会导致某个时间段运行帧率变慢。
我们可以设想下这个情况。假如小车每帧往前移动 1 个单位,第一秒内如果程序的帧率正常(60fps),这一秒小车就会移动 60 个单位。到第二秒开始,若场景里出现很多元素,导致程序帧率变成 20 fps了,由于小车每帧累加的值是固定的,所以这一秒,小车就只移动了 20 个单位。合起来,在两秒的时间中,小车只移动了 80 个单位。相比帧率恒定的情况下移动 120 个单位,小车移动的距离明显变小了。而且整个动画连起来看,小车做的就不再是匀速运动,出现先快后慢的结果。
为了避免这种情况,如果用基于时间的写法,效果就大有不同。因为这时小车的位置是根据时间的流逝多少决定的。它能保证在相应时间内,小车的位置都在“正确”的地方。只是帧率低的时候,画面运动的流畅度降低而已,整体小车的运行速度并没有变化。
基于这种特性。游戏中的运动基本是采用基于时间的算法去实现的。毕竟不同玩家的电脑配置可能有很大的区别,如果开发一个赛场游戏,汽车运动算法是基于帧的。那电脑配置高的玩家,车的速度就变快,这显然是不合理的。
运动的自然之道 - 使用函数曲线
我们再来看前面写的小球动画。虽然它是动起来了,但显得很呆板。为何会产生这种感觉?这是因为违背了人的视觉经验。在日常生活中,我们很难看见一个物体从完全静止的状态突然变成匀速运动的状态,也很难看到一个运动中的物体瞬间静止。
要改善这种状况,一个简单的方式是引入“力”。比如下面的例子,实现了小球从静止到加速。
代码 03 ( 加速运动 )
float posX;
float acc;
float vel;
void setup(){
size(600,200);
posX = 100;
}
void draw(){
background(239,234,228);
acc = 0.5;
vel += acc;
posX += vel;
if(posX > 500){
posX = 500;
}
fill(50,120,133);
noStroke();
ellipse(posX,height/2,50,50);
}
如果希望上面的小球在快接近目标的时候有减速的效果,就需要在上面增加一些属性或是添加判定条件。这样的做法显然有些繁琐,而且仍旧是“基于帧”的。如果我们希望准确地控制小球的运动时间,仅用上面代码是无法做到的。
有更简便的方式吗?函数曲线此时就可以派上用场。
函数曲线
我们先选一个典型的数学函数 sin
再结合图像理解下面代码
代码 04 ( 加速到减速 )
float time;
void setup(){
size(600,200);
time = 3;
}
void draw(){
background(239,234,228);
float sinInput = map(min(time,millis()/1000.0),0,time,-PI/2,PI/2);
float x = map(sin(sinInput),-1,1,100,500);
fill(50,120,133);
noStroke();
ellipse(x,height/2,50,50);
}
代码浅析:
相比代码 03,例子 04 并没有用到速度,加速度等变量。但仍然可以看到小球有加速,减速的运动变化,而且可以通过 time 变量去控制小球的运动时间
虽然运动并不严格遵循牛顿力学,但整体效果还是比较自然的。它很巧妙地利用了 sin 函数曲线的变化来映射小球的位置变化。具体的操作,是在 x 方向上截取一段合适的区间,然后将对应函数值 y 的变化,映射到我们需要的变化区间之内。若有模糊的地方,可以对照下图去理解
蓝线可以看成是“时间”(时间流逝速率恒定)。请脑补一个动画,蓝线以恒定的速度从 -0.5 π 的位置从左往右移动到 0.5 π 的位置。它与函数曲线的交点为 A。此时 A 点的 y 坐标就表示函数的输出值。可以看出在这个区间内移动,sin 函数的输出值就会从 -1 变化到 1。但这个输出的变化值我们不能直接使用,需要通过 map 函数,将它映射到在我们想要的范围内变化。
sin 函数在这里其实就是一个中转站。只是使用它前,需要将输入值和输出值做两次处理 (调用两次 map)。第一次调用 map 函数,就是将时间从 0 到 time 的变化,映射为 -PI/2 到 PI/2 之间的变化,再传入函数中。第二次调用 map,则是函数的输出值映射为我们需要的位置数值。
同一个函数,选择的输入区间不同。得出的结果也不同。假如选择从 A 点到 B 点作为变化区间,整体的运动速率就是先慢后快的加速过程。如果选择从 B 点到 C 点,则整体的运动速率就是先快后快的减速过程。要判断是加速还是减速,可以对照函数曲线。越平的地方,就代表运动越慢,越陡峭,就表明运动变化越快。
指数函数
当理解了上面的思路。现在数学函数就可以成为你的创作素材。常用的数学函数除了三角函数 sin,cos。还有指数函数。
一般地,y = a^x函数(a为常数且以a>0,a≠1)叫做指数函数。下图是 y=2^x 的图像。
指数函数在 Processing 中写作
pow(a,b)
其中 a 表示底数,b 表示指数。pow(2,2) 表示 2 的 3 次方,结果为 8。
有关指数函数的用法就不再展开,与上面例子是类似,找准输入输出区间再作映射即可。函数曲线的使用是非常灵活的。不仅可以单独使用,还可以组合使用。例如两个基本函数进行相加和相成,都会得到意想不到的效果。
延展
现在仅仅靠指数函数与三角函数,就可以产生各种不同的函数曲线。下面代码就是指数函数与三角函数的叠加,它使得小球加速靠近的同时,能有一个来回的摆动。最终产生了带弹性的动画效果。
float inputVal = min(map(millis()/1000.0,0,time,0,1),1);
float x = map(cos(inputVal * 20) * pow(2,-10.0 * inputVal),1,0,100,500);
( 替换例 04 的运动算法 )
End
要尽可能理解函数曲线的特性。就需要多加实验。函数曲线可不仅仅只能用在运动动画上。下面用了 5 种常用函数输出了几组 gif。分别控制图形的位置,颜色,旋转角度,大小。可以去从中感受不同函数曲线的个性。
【 1 】线性递增(匀速变化)
【 2 】sin 函数(区间 -PI/2 到 PI/2,从加速到减速)
【 3 】指数函数(减速)
【 4 】指数函数叠加 cos 函数(整体减速)
【 5 】sin 函数(往复)
( 控制位置 )
( 控制透明度 )
( 控制旋转角度 )
( 控制大小 )
最后附上一张由 Kynd 整理的一张图,里面的函数曲线都很实用,有兴趣可以到此地址下载高清大图,了解更多函数曲线 ( http://thebookofshaders.com/05/kynd.png )